/**
 * Copyright 2010 Philippe Beaudoin
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.puzzlebazar.client.ui;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.inject.Inject;
import com.puzzlebazar.client.resources.Resources;
import com.puzzlebazar.shared.util.Has2DSize;
import com.puzzlebazar.shared.util.Vec2i;

/**
 * <b>Important!</b> This layout panel must be added inside a {@link com.google.gwt.user.client.ui.LayoutPanel}.
 * <p />
 * This widget represents a square grid and is often useful to draw puzzles that use such grids.
 * It holds the square grid and the desired border around it.
 *
 * @author Philippe Beaudoin
 */
public class SquareGridLayoutPanel extends AspectRatioLayoutPanel implements Has2DSize {

  private static class CellLayoutPanel extends OverflowLayoutPanel {

    /**
     * Adds a widget that should occupy the top-left vertex.
     *
     * @param widget The widget that should occupy the top-left vertex.
     * @param thickness Its thickness, in pixels.
     */
    private void addVertexWidget(Widget widget, int thickness) {
      add(widget);
      int halfThickness = thickness / 2;
      setWidgetLeftWidth(widget, -halfThickness, Unit.PX, thickness, Unit.PX);
      setWidgetTopHeight(widget, -halfThickness, Unit.PX, thickness, Unit.PX);
    }

    /**
     * Adds a widget that should occupy the left edge.
     *
     * @param widget The widget that should occupy the left edge.
     * @param thickness Its thickness, in pixels.
     */
    private void addLeftEdgeWidget(Widget widget, int thickness) {
      add(widget);
      int halfThickness = thickness / 2;
      int thicknessRemainder = thickness - halfThickness;
      setWidgetLeftWidth(widget, -halfThickness, Unit.PX, thickness, Unit.PX);
      setWidgetTopBottom(widget, -halfThickness, Unit.PX, -thicknessRemainder + 1, Unit.PX);
    }

    /**
     * Adds a widget that should occupy the top edge.
     *
     * @param widget The widget that should occupy the top edge.
     * @param thickness Its thickness, in pixels.
     */
    private void addTopEdgeWidget(Widget widget, int thickness) {
      add(widget);
      int halfThickness = thickness / 2;
      int thicknessRemainder = thickness - halfThickness;
      setWidgetTopHeight(widget, -halfThickness, Unit.PX, thickness, Unit.PX);
      setWidgetLeftRight(widget, -halfThickness, Unit.PX, -thicknessRemainder + 1, Unit.PX);
    }

    /**
     * Adds a widget that should occupy the cell.
     *
     * @param widget The widget that should occupy the cell.
     */
    private void addCellWidget(Widget widget) {
      add(widget);
    }
  }

  private final Resources resources;

  private int border;
  public int width;
  public int height;

  // This container holds the square grid elements themselves, such as
  // grid cells or grid edges.
  private OverflowLayoutPanel squareGridContainer;

  private CellLayoutPanel cells[][];

  @Inject
  public SquareGridLayoutPanel(Resources resources) {
    super();

    this.resources = resources;

    squareGridContainer = GWT.create(OverflowLayoutPanel.class);
    add(squareGridContainer);
    setBorder(border);
  }

  /**
   * Changes the desired border between the edges of the square grid
   * and that of its container. This border will allow the square grid
   * to draw elements slightly outside itself. It's useful for drawing
   * thicker outside lines, for example.
   *
   * @param border The desired border size, in pixels.
   */
  public void setBorder(int border) {
    // The 1 extra pixel on the right and bottom is so that elements positioned at 100%
    // within the puzzleContainer fall inside the mainContainer.
    setWidgetLeftRight(squareGridContainer, border, Unit.PX, border + 1, Unit.PX);
    setWidgetTopBottom(squareGridContainer, border, Unit.PX, border + 1, Unit.PX);
    super.setBorder(border);
  }

  /**
   * Sets the size of the square grid. All the grid content will be lost.
   *
   * @param width The desired number or grid cells along the horizontal direction.
   * @param height The desired number or grid cells along the vertical direction.
   */
  public void setSize(int width, int height) {
    this.width = width;
    this.height = height;
    setAspectRatio(width / (float) height);
    createCellPanels();
  }

  @Override
  public int getWidth() {
    return width;
  }

  @Override
  public int getHeight() {
    return height;
  }

  /**
   * Adds a the widget to draw at a specific vertex.
   * (See {@link com.puzzlebazar.shared.util.Recti} for details on cells and vertices.)
   *
   * @param loc The location of this vertex (a {@link Vec2i}).
   * @param widget The {@link Widget} to set.
   * @param thickness The desired thickness for this edge (in pixels).
   */
  public void addVertexWidget(Vec2i loc, Widget widget, int thickness) {
    assert 0 <= loc.x && loc.x <= width : "The x coordinate must be a valid vertex coordinate.";
    assert 0 <= loc.y && loc.y <= height : "The y coordinate must be a valid vertex coordinate.";
    cells[loc.x][loc.y].addVertexWidget(widget, thickness);
  }

  /**
   * Adds a the widget to draw at a specific vertical edge.
   *
   * @param loc The location of this vertical edge. A {@link Vec2i} where the
   *            x coordinate is a vertex coordinate and the y coordinate is a cell coordinate.
   *            (See {@link com.puzzlebazar.shared.util.Recti} for details on cells and vertices.)
   * @param widget The {@link Widget} to set.
   * @param thickness The desired thickness for this edge (in pixels).
   */
  public void addVerticalEdgeWidget(Vec2i loc, Widget widget, int thickness) {
    assert 0 <= loc.x && loc.x <= width : "The x coordinate must be a valid vertex coordinate.";
    assert 0 <= loc.y && loc.y < height : "The y coordinate must be a valid cell coordinate.";
    cells[loc.x][loc.y].addLeftEdgeWidget(widget, thickness);
  }

  /**
   * Adds a widget to draw at a specific horizontal edge.
   *
   * @param loc The location of this horizontal edge. A {@link Vec2i} where the
   *            x coordinate is a cell coordinate and the y coordinate is a vertex coordinate.
   *            (See {@link com.puzzlebazar.shared.util.Recti} for details on cells and vertices.)
   * @param widget The {@link Widget} to set.
   * @param thickness The desired thickness for this edge (in pixels).
   */
  public void addHorizontalEdgeWidget(Vec2i loc, Widget widget, int thickness) {
    assert 0 <= loc.x && loc.x < width : "The x coordinate must be a valid cell coordinate.";
    assert 0 <= loc.y && loc.y <= height : "The y coordinate must be a valid vertex coordinate.";
    cells[loc.x][loc.y].addTopEdgeWidget(widget, thickness);
  }

  /**
   * Adds a the widget to draw at a specific cell.
   * (See {@link com.puzzlebazar.shared.util.Recti} for details on cells and vertices.)
   *
   * @param loc The location of this cell (a {@link Vec2i}).
   * @param widget The {@link Widget} to set.
   */
  public void addCellWidget(Vec2i loc, Widget widget) {
    assert 0 <= loc.x && loc.x < width : "The x coordinate must be a valid cell coordinate.";
    assert 0 <= loc.y && loc.y < height : "The y coordinate must be a valid cell coordinate.";
    cells[loc.x][loc.y].addCellWidget(widget);
  }

  /**
   * Creates a standard vertex widget and adds it at a specific vertex.
   * (See {@link com.puzzlebazar.shared.util.Recti} for details on cells and vertices.)
   *
   * @param loc The location of this vertex (a {@link Vec2i}).
   * @param thickness The desired thickness for this edge (in pixels).
   * @param styleNames A list of string containing all the desired style names. (The default edge style is added automatically.)
   * @return The newly created widget.
   */
  public Widget createVertex(Vec2i loc, int thickness, String... styleNames) {
    Widget widget = GWT.create(FlowPanel.class);
    widget.addStyleName(resources.style().vertex());
    for (String style : styleNames) {
      widget.addStyleName(style);
    }
    addVertexWidget(loc, widget, thickness);
    return widget;
  }

  /**
   * Creates a standard edge widget and adds it to the square
   * at a specific vertical edge.
   *
   * @param loc The location of this horizontal edge. A {@link Vec2i} where the
   *            x coordinate is a cell coordinate and the y coordinate is a vertex coordinate.
   *            (See {@link com.puzzlebazar.shared.util.Recti} for details on cells and vertices.)
   * @param thickness The desired thickness for this edge (in pixels).
   * @param styleNames A list of string containing all the desired style names. (The default edge style is added automatically.)
   * @return The newly created widget.
   */
  public Widget createHorizontalEdge(Vec2i loc, int thickness, String... styleNames) {
    Widget widget = GWT.create(FlowPanel.class);
    widget.addStyleName(resources.style().edge());
    for (String style : styleNames) {
      widget.addStyleName(style);
    }
    addHorizontalEdgeWidget(loc, widget, thickness);
    return widget;
  }

  /**
   * Creates a standard edge widget and adds it to the square
   * at a specific horizontal edge.
   *
   * @param loc The location of this vertical edge. A {@link Vec2i} where the
   *            x coordinate is a vertex coordinate and the y coordinate is a cell coordinate.
   *            (See {@link com.puzzlebazar.shared.util.Recti} for details on cells and vertices.)
   * @param thickness The desired thickness for this edge (in pixels).
   * @param styleNames A list of string containing all the desired style names. (The default edge style is added automatically.)
   * @return The newly created widget.
   */
  public Widget createVerticalEdge(Vec2i loc, int thickness, String... styleNames) {
    Widget widget = GWT.create(FlowPanel.class);
    widget.addStyleName(resources.style().edge());
    for (String style : styleNames) {
      widget.addStyleName(style);
    }
    addVerticalEdgeWidget(loc, widget, thickness);
    return widget;
  }

  /**
   * Creates a standard cell widget and adds it at a specific cell.
   * (See {@link com.puzzlebazar.shared.util.Recti} for details on cells and vertices.)
   *
   * @param loc The location of this cell (a {@link Vec2i}).
   * @param styleNames A list of string containing all the desired style names. (The default cell style is added automatically.)
   * @return The newly created widget.
   */
  public Widget createCell(Vec2i loc, String... styleNames) {
    Widget widget = GWT.create(FlowPanel.class);
    widget.addStyleName(resources.style().cell());
    for (String style : styleNames) {
      widget.addStyleName(style);
    }
    addCellWidget(loc, widget);
    return widget;
  }

  /**
   * Creates a standard selected cell widget and adds it at a specific cell.
   * (See {@link com.puzzlebazar.shared.util.Recti} for details on cells and vertices.)
   *
   * @param loc The location of this cell (a {@link Vec2i}).
   * @param styleNames A list of string containing all the desired style names. (The default selected cell style is added automatically.)
   * @return The newly created widget.
   */
  public Widget createSelectedCell(Vec2i loc, String... styleNames) {
    Widget widget = GWT.create(FlowPanel.class);
    widget.addStyleName(resources.style().selectedCell());
    for (String style : styleNames) {
      widget.addStyleName(style);
    }
    addCellWidget(loc, widget);
    return widget;
  }

  /**
   * Creates and adds all the inner horizontal and vertical edges to draw
   * a regular grid.
   *
   * @param thickness The desired thickness of the inner edges.
   * @param styleNames A list of string containing all the desired style names. (The default edge style is added automatically.)
   * @return A list of all newly created widgets.
   */
  public List<Widget> createInnerEdges(int thickness, String... styleNames) {
    Vec2i loc = new Vec2i();
    List<Widget> result = new ArrayList<Widget>();
    for (int y = 0; y < height; ++y) {
      loc.y = y;
      for (int x = 0; x < width; ++x) {
        loc.x = x;
        if (x > 0) {
          result.add(createVerticalEdge(loc, thickness, styleNames));
        }
        if (y > 0) {
          result.add(createHorizontalEdge(loc, thickness, styleNames));
        }
      }
    }
    return result;
  }

  /**
   * Creates and adds all the outer horizontal and vertical edges to draw
   * a regular grid.
   *
   * @param thickness The desired thickness of the inner edges.
   * @param styleNames A list of string containing all the desired style names. (The default edge style is added automatically.)
   * @return A list of all newly created widgets.
   */
  public List<Widget> createOuterEdges(int thickness, String... styleNames) {
    Vec2i loc = new Vec2i();
    List<Widget> result = new ArrayList<Widget>();
    for (int y = 0; y <= height; y += height) {
      loc.y = y;
      for (int x = 0; x < width; ++x) {
        loc.x = x;
        result.add(createHorizontalEdge(loc, thickness, styleNames));
      }
    }
    for (int x = 0; x <= width; x += width) {
      loc.x = x;
      for (int y = 0; y < height; ++y) {
        loc.y = y;
        result.add(createVerticalEdge(loc, thickness, styleNames));
      }
    }
    return result;
  }

  /**
   * Clears all the content and creates an {@link OverflowLayoutPanel} for every cell.
   * In addition, similar {@link OverflowLayoutPanel}s are created pass the right-hand
   * column and pass the bottom line. This makes it possible to draw edges and vertices
   * element on the right and bottom edge of the puzzle.
   */
  private void createCellPanels() {
    final int precision = 1024;
    squareGridContainer.clear();
    cells = new CellLayoutPanel[width + 1][height + 1];
    for (int y = 0; y <= height; ++y) {
      int percentY = y * precision / height;
      int percentNextYBottom = precision - (y + 1) * precision / height;
      for (int x = 0; x <= width; ++x) {
        int percentX = x * precision / width;
        int percentNextXRight = precision - (x + 1) * precision / width;
        CellLayoutPanel cell = GWT.create(CellLayoutPanel.class);
        cells[x][y] = cell;
        squareGridContainer.add(cell);
        squareGridContainer.setWidgetLeftRight(cell,
            percentX * 100.0 / precision, Unit.PCT,
            percentNextXRight * 100.0 / precision, Unit.PCT);
        squareGridContainer.setWidgetTopBottom(cell,
            percentY * 100.0 / precision, Unit.PCT,
            percentNextYBottom * 100.0 / precision, Unit.PCT);
      }
    }
  }

}
